<?php
/**
 * 
 */
abstract class Model extends Base implements IteratorAggregate {
	
	//模型是否有数据库表
	protected $hasTb = true;
	
	//当前model的名称
	protected $modelName;
	
	// 数据库的名称
	protected $dbName;
	
	//当前表名(不含前缀)
	protected $tbName;	
	
	//数据表前缀
	protected $tbPrefix;
	
	//回收标志的字段名
	protected $recycleField = 'recycle';

	// 主键名
	protected $pk;
	
	// 数据库操作对象
	private $db = null;
	
    // 数据信息
    protected $data =   array();
    
    // 数据列表信息
    protected $dataList =   array();
    
    /**
     * 对are类型实现迭代模式
     * @return ArrayObject
     */
    function getIterator() {
        return new ArrayObject($this->dataList);
    }
    
	/**
	 * 构造函数
	 * @param string|array $pkv
	 * @return void
	 * */
	function __construct($pkv = null)
	{
		$pkv = func_get_arg(1);
        $this->_init();
        $this->be($pkv);
	}
	/**
	 *
	 * @param string $pkv
	 * @return mix
	 */
	function be($pkv){
        if($pkv != null){
	      return strpos($pkv,',') === false 
	        ? $this->_is($pkv) 
	        : $this->_are($pkv);
        }
	}

	/**
	 * 成为单行对象
	 * @param string $pkv
	 * @return array
	 */
	private function _is($pkv)
	{
		//把多行对象的数据清除
		$this->dataList = array();
		$this->data = $this->selectRow("*", "where `{$this->pk}` = '$pkv'");
		return $this->data;
	}
	
	/**
	 * 成为多行对象
	 * @param string|array $pkvs
	 * @return array
	 */
	private function _are($pkvs){
		//把单行对象的数据清除
		$this->data = array();
		$this->dataList = $this->select("*", "where `{$this->pk}` = '$pkv'");
		return $this->dataList;
	}

    /**
     * 设置数据单行对象的值
     * @param string $name 
     * @param mixed $value 
     * @return void
     */
    function __set($name,$value) {
        $this->data[$name]  =   $value;
    }

    /**
     * 获取数据单行对象的值
     * @param string $name
     * @return mixed
     */
   function __get($name) {
    	return isset($this->data[$name]) 
    		  ? $this->data[$name]
    		  : null ;
    }
    
	/**
	 * 初始化模型
	 */
	private function _init(){
		
        // 获取数据库操作对象
        if(!empty($this->connection)) {
            // 当前模型有独立的数据库连接信息
            $this->db = Db::getInstance($this->connection);
        }else{
        	
            $this->db = Db::getInstance();
        }
        
        // 设置表前缀
        empty($this->tbPrefix) && $this->tbPrefix = C('TB_PREFIX');

        // 设置数据库名
        empty($this->dbName) && $this->dbName = C('DB_NAME');
		
        if($this->hasTb){
			//设置表名
			$this->_setTbName();
        	//设置主键名
			empty($this->pk) && $this->pk = $this->getPkName();
        }
        
	}
	
	/**
	 * 设置表名。如果没有在model中指定表名，则根据模型的名称安管理返回.
	 */
	private function _setTbName()
	{
		if(empty($this->tbName)){
			$modelName = $this->getModelName();
        	$this->tbName = trim(strtolower(preg_replace("'([A-Z])'",'_$1',$modelName)),"_");
		}
	}
	
	/**
	 * 返回带前缀，数据库名 的数据表名
	 * @param string $tbname 不带前缀的数据表名
	 * @return string
	 * */
	function tbName($tbname = "")
	{
		if(empty($tbname)){
			$tbname = $this->tbName;
		} 
		if(empty($tbname)){
			throw new GiException("Table name is not specified!");
		}
		return "`{$this->dbName}`.`{$this->tbPrefix}$tbname`";
	}
	
	function selectBySql($sql){
		_trace($sql);
		$rs = $this->db->query($sql);
		$data = array();
		while ($data = $this->db->fetchArray($rs)) {
			$this->_after_select_fetch($data);
			$result[] = $data;
		}
		return $result;
	}
	/**
	 * 从数据表选择数据
	 * @param string|array $fields 要选择的字段名
	 * @param string $ext 附加语句
	 * @param string $tbname 数据表名 默认为在扩展类中定义的表名
	 * 
	 * @return array
	 * */
	function select($fields = "*", $ext="", $tbname="")
	{
		if ( "" == $tbname) {
			$tbname = $this->tbName;
		}
		
		if (is_array($fields)) {
			$fields = '`' . join('`,`',$fields) . '`';
		}
		
		$result = array();
		$tbname  = $this->TbName($tbname);
		$sql= "select $fields from $tbname $ext";
		return $this->selectBySql($sql);
	}
	/**
	 * 执行select时，在fetch每条记录后对数据做处理
	 */
	protected function _after_select_fetch(&$row){}
	/**
	 * 从数据表选择一条数据
	 * @param string $fields 要选择的字段名
	 * @param string $ext 附加语句
	 * @param string $tbname 数据表名 默认为在扩展类中定义的表名
	 * 
	 * @return array
	 * */
	function selectRow($fields = "*", $ext="" , $tbname = "")
	{
		$data = $this->Select($fields, $ext, $tbname);
		$ret = $data[0] ? $data[0] : array();
		return $ret;
	}
	
	/**
	 * 从数据表读取一个值
	 * @param string $fields
	 * @param string $ext
	 * @param string $tbname
	 * @return mix
	 */
	function selectVar($fields, $ext = "", $tbname = "")
	{
		$data = $this->Select($fields, $ext, $tbname);
		if (is_array($data[0])) {
			return current($data[0]);
		} else {
			return false;
		}
	}
	
	/**
	 * 统计符合条件的记录的数量
	 *
	 * @param string $ext 附加语句
	 * @param string $tbname 数据表名 默认为在扩展类中定义的表名
	 * 
	 * @return int
	 * */	
	function selectCount($ext="",$tbname = ""){
		$data = $this->SelectVar("count(*) as `total`", $ext, $tbname);
		return intval($data);
	}
	
	/**
	 * 向数据库插入一条记录
	 * @param array  $data   要插入的数据 
	 * @param string $tbname 数据表名  默认为在扩展类中定义的表名
	 * 
	 * @return int 插入列的id号
	 * */
	function insert($data = null,$tbname = ""){
		return $this->_insertOrReplace('insert', $data, $tbname);
	}

	/**
	 * 向数据库替换一条记录
	 * @param array  $data   要处理的数据 
	 * @param string $tbname 数据表名  默认为在扩展类中定义的表名
	 * 
	 * @return int 列的id号
	 * */
	function replace($data,$tbname = ""){
		return $this->_insertOrReplace('replace', $data, $tbname);
	}

	/**
	 * 向数据库插入或替换一条记录
	 * @param array  $data   要处理的数据 
	 * @param string $tbname 数据表名  默认为在扩展类中定义的表名
	 * 
	 * @return int 插入列的id号
	 * */
	private function _insertOrReplace($action, $data, $tbname = ""){

		$data === null && $data = $this->data;
		
		if($action != "replace"  && $action != "insert"){
			_halt("Base::InsertOrReplace 的\$action参数不能为{$action}。");
		}

		if ( "" == $tbname) {
			$tbname = $this->tbName;
			"" == $tbname && _halt("Insert::没有指定表名.");
		}

		//some table may needs this field when doing insert.
		if(empty($data['create'])) $data['create'] = time();
		if(empty($data['create_ipaddress'])) $data['create_ipaddress'] = _getip();
		
		//过滤不需要的字段
		$this->prepareData($data,$tbname);
		
		//add prefix for table name 
		$tbname  = $this->TbName($tbname);
		
		//准备插入语句
		$fields = array();
		$values = array();
		foreach ($data as $key => $value){
			$fields[] = '`' . $key . '`' ;
			$values[] = "'" .  $value . "'";
		}
		$fields_s = implode(',',$fields);
		$values_s = implode(',',$values);
		
		unset($fields,$values);
		
		$sql = "$action into $tbname ($fields_s) values ($values_s)";
		
		//执行插入
		$this->db->query($sql);
		
		return $this->db->insertId();
	}

	/**
	 * 按照主键的值更新数据库中的记录.主键的值可以是包含多个id的数组也可以是一个不为0的整数
	 * @param array  $data   要插入的数据 
	 * @param string $tbname 数据表名[默认为继承该类的子类的表名]
	 * 
	 * @return bool
	 * */
	function update($data=null,$tbname = ""){

		$data === null && $data = $this->data;
		
		$pk = $this->pk;
		
		//更新数据时必须应包含主键字段
		if ( !isset($data[$pk]) ) {
			throw new GiException("要更新的数据必须应包含 {$pk} 字段");
			return false;
		}

		//形成更新的where语句
		if (!is_array($data[$pk])) {
			$data[$pk]=array($data[$pk]);
		}

		//主键的值必须是不为0的整数
		foreach ($data[$pk] as $key => $value){
			$data[$pk][$key] = mysql_escape_string($value);
		}
		
		$where = "where `$pk` in ('" . implode("','",$data[$pk]) ."')";
		
		unset($data[$pk]);
		return $this->updateByCondition($data,$where,$tbname);
	}

	/**
	 * 更新数据库中的记录.
	 * 使用该方法时要考虑$ext可能造成sql注入.应尽量使用本类中的update的方法
	 *
	 * @param array  $data   要插入的数据
	 * @param string $ext	 查询的where语句
	 * @param string $tbname 数据表名
	 * 
	 * @return int 受影响的记录数
	 * 
	 **/
	function updateByCondition($data, $ext, $tbname = ""){

		
		if ( "" == $tbname) {
			$tbname = $this->tbName;
		}

		//$ext不能为空
		if ( "" == trim($ext) ) {
			_halt("该更新语句没有限定条件,不被允许执行");
			return false;
		}
		//Add default value
		if (empty($data['updated'])) {
			$data['updated'] = time();
		}
		//准备更新数据
		$this->prepareData($data,$tbname);
		$sql_pieces = array();
		foreach ($data as $key => $value){
			$sql_pieces[] = "`$key`='$value'";
		}

		//为表名和数据库名加前缀
		$tbname = $this->TbName($tbname);
		
		//形成sql语句
		$sql = "update $tbname set " . implode(',',$sql_pieces) . " $ext";
	
		//Exec
		$this->db->query($sql);
		
		return $this->db->affectedRows();
	}

	/**
	 * 把符合条件的记录删除
	 * @param array  $data   要删除的ID列表
	 * @param string $ext	 查询的where语句
	 * @param string $tbname 数据表名
	 * @return int 受影响的记录数
	 * */
	function remove($id_list=null,  $tbname=""){
		if(empty($id_list)){
			throw new GiException("Id for remove is empty.");
		}		
		

		
		//主键键名
		$pk = $this->pk;
		
		
		if(empty($pk)){
			throw new GiException("PK for remove is not specified!");
		}
		
		//单行对象时直接删除对应记录
		$id_list === null && $id_list = $this->data[$pk];
		

		
		//形成更新的where语句
		if (!is_array($id_list)) {
			$id_list = explode(',',$id_list);
		}		

		//主键的值必须是不为0的整数
		foreach ($id_list as $key => $value){
			$id_list[$key] = (int)$value;
			if(0 == $value){
				unset($id_list[$key]);
			}
		}
		
		//主键值不能为空
		if(count($id_list) == 0 ){
			throw new GiException("\ID for remove is not ready!");
		}			
		//形成$ext语句
		$ext = "where `$pk` in (" . implode(",",$id_list) .")";

		return $this->removeByCondition($ext, $dbname,$tbname);		
	}

	/**
	 * 把符合条件的记录删除
	 * 使用该方法时要考虑$ext可能造成sql注入.应尽量使用本类中的其他Remove的方法,例如RemoveById();
	 * @param string $ext 查询的where语句
	 * @param string $tbname 数据表名
	 * @return int 受影响的记录数
	 **/
	function removeByCondition($ext, $tbname = ""){
		
		//默认表名
		if ($tbname == "") {
			$tbname = $this->tbName;
		}
		
		//$ext不能为空
		if ( "" == trim($ext) ) {
			_halt("该删除语句没有限定条件,不被允许执行");
			return false;
		}
		
		//为表名加前缀
		$tbname = $this->TbName($tbname);
		
		$this->db->query("delete from $tbname $ext");
		return $this->db->affectedRows();
	}

	/**
	 * 向数据库保存一条记录(插入或者更新)
	 * @param array  $data   要处理的数据 
	 * @param string $tbname 数据表名  默认为在扩展类中定义的表名
	 * 
	 * @return mix 如果为Insert则返回列的id号，如果为Update则返回受影响的记录数量
	 * */
	function save($data,$tbname = ""){
		if(empty($data[$this->pk])){
			return $this->Insert($data,$tbname);
		} else {		
			return $this->Update($data,$tbname);
		}
	}
	/**
	 * 保存多条记录
	 * @param array $data
	 * @param string $tbname
	 * @return int
	 */
	function saveAll($data, $tbname="")
	{
		$cnt = 0;
		foreach ($data as $row){
			$this->save($row, $tbname) && $cnt++;
		}
		return $cnt;
	}

	/**
	 * 把表清空
	 * @param string $dbname 数据库名
	 * */
	function cleanTable( $tbname = ""){
		
		if ($tbname == "") {
			$tbname = $this->tbName;
		}
		
		$tbname = $this->TbName($tbname);
		
		$sql= "TRUNCATE TABLE $tbname";
		return $this->db->query($sql);
	}
	
	/**
	 * 把符合条件的记录放进回收站
	 * 使用该方法时要考虑$ext可能造成sql注入.
	 * @param string $ext	 查询的where语句
	 * @param string $dbname 数据库名
	 * @param string $tbname 数据表名
	 * */
	function toRecycleByCondition($ext, $tbname = ""){
		$data = array();
		$data[$this->recycleField] = 1;
		$this->updateByCondition($data, $ext, $tbname);
	}
	
	/**
	 * 把指定ID(ID集)的记录放进回收站
	 * @param mix $idList	 要修改的ID号,可以为非零整数获数组
	 * @param string $dbname 数据库名
	 * @param string $tbname 数据表名
	 * */
	function toRecycle($idList, $tbname="")	{
		$data[$this->recycleField] = 1;
		$ext = "where `{$this->pk}` in ($idList)";
		$this->updateByCondition($data, $ext, $tbname);
	}

	/**
	 * 把符合条件的记录从回收站还原出来
	 * 使用该方法时要考虑$ext可能造成sql注入.
	 * @param string $ext	 查询的where语句
	 * @param string $dbname 数据库名
	 * @param string $tbname 数据表名
	 * */
	function outRecycleByCondition($ext, $tbname = ""){
		$data[$this->recycleField] = 0;
		Update($data, $ext, $tbname);
	}
	
	/**
	 * 把指定ID(ID集)的记录从回收站还原出来
	 * @param mix $id	 要修改的ID号,可以为非零整数获数组
	 * @param string $dbname 数据库名
	 * @param string $tbname 数据表名
	 * */
	function outRecycle($idList, $tbname="")	{
		$data[$this->recycleField] = 0;
		$ext = "where `{$this->pk}` in ($idList)";
		$this->updateByCondition($data, $ext, $tbname);
	}

	/**
	 * 清空指定数据库的数据表的回收站
	 * @param string $dbname 数据库名
	 * @param string $tbname 数据表名
	 * */		
	function cleanRecycle($tbname = ""){
		$ext = "where `" . $this->recycleField . "` = '1'";
		$this->Remove($ext,$tbname);
	}

	/**
	 * 为插入和更新记录准备数据,去除不必要的列
	 * @param &array &$data  数据数组
	 * @param string $tbname 表的名称
	 * @return array
	 * */
	function prepareData(&$data,$tbname = ""){
		if ( "" == $tbname) {
			$tbname = $this->tbName;
		}
		$tbname = $this->tbName($tbname);
		if(!$tb_fields = _getCache("tbfields_$tbname")){
			$tb_fields = $this->db->getTbFields($tbname);
			_writeCache("tbfields_$tbname",$tb_fields);
		}
		foreach ($data as $key => $value){
			if (!in_array($key,$tb_fields) || is_array($value)) {
				//去掉多余的列,值为数组的列. 对键名不用加反斜杠
				unset($data[$key]);
			} else {
				//避免$value中存在注入风险
				$data[mysql_real_escape_string($key)] = mysql_real_escape_string(trim($value));
			} 
		}
		if(0 == count($data)){
			_halt("在准备数据时数据数组为空.");
		}
		return $data;
	}
	
	/**
     * 得到当前的模型的名称
     * @return string
     */
    public function getModelName()
    {
        if(empty($this->modelName)) {
            $this->modelName = substr(get_class($this),6);
        }
        return $this->modelName;
    }
    /**
     * 从数据库获取当前模型的主键名称
     * @param string $tbname  不含前缀的表名
     * @return string
     */
    function getPkName($tbname = ""){
    	$tbname == "" && $tbname = $this->tbName;
    	return $this->db->getPkName($this->tbName($tbname));
    }
    
    /**
     * 按照like exact date range将所有的搜索条件形成sql可用的conditions数组
     *
     * @param array $like
     * @param array $exact
     * @param array $date
     * @param array $range
     * @return array
     */
    static function phaseConditions($like = null, $exact = null, $date = null, $range = null){
    	
    	$data = array();
    	$conditions = array();
    	
    	is_array($like) && $data['like'] = $like;
    	is_array($exact) && $data['exact'] = $exact;
    	is_array($date) && $data['date'] = $date;
    	is_array($range) && $data['range'] = $range;
    	
    	foreach ($data as $type => $elements){
	    	foreach ($elements as $name => $keywords){
	    		$condition = self::_phaseCondition($name,$keywords,$type);
	    		$condition && $conditions[] = $condition;
	    	}
    	}
    	
    	return $conditions;
    }
    /**
     * 按照给定的参数形成一个sql条件
     *
     * <code>
     * self::_phaseCondition('name','张三 李四','like')；
     * //`name` like '张三' and `name` like '李四'
     * </code>
     * 
     * @param string $name
     * @param string keywords 要搜索的关键字，多个关键字用“空格”隔开
     * @param string $type exact|like|date|range
     * @return string 
     */
    
    private static function _phaseCondition($name,$keywords,$type,$andOr = "and")
    {
    	if(!in_array(strtolower($andOr),array("and","or"))) throw new GiException("$andOr is not a valid parameter.");
		
		if($keywords == '') return "";

		$conditions = explode(' ',$keywords);
		switch ($type) {
			case "exact":
				$condition_e = "'" . @implode("','",$conditions) . "'";
				$sql = "`$name` in ($condition_e)";
				break;
			case "like":
				foreach ($conditions as $condition){
					$condition = trim($condition);
					$sql[] = "`$name` like '%$condition%'";
				}		
				$sql = implode(" $andOr ", $sql);
				break;
			case "date":			
				foreach ($conditions as $condition){					
					$date_array = explode('~',$condition);
					$date_array = trim_all($date_array);
					
					unset($condition);
					
					if($date_array[0])
						$condition[] = "`$name` >= '$date_array[0]'";
					
					if($date_array[1])
						$condition[] = "`$name` <= '$date_array[1]'";
	
					$sql[] = "(" . implode(" and ", $condition) . ")";
					
				}
				$sql = implode(" $andOr ", $sql);
				break;	
			case "range":
				foreach ($conditions as $condition){
					$keys = array(">","<","~","=");
					//~ is found in condition string
					if( strpos($condition,"~") > -1 ){
						
						$data_array = explode('~',$keywords);
						
						unset($condition);
						if(!empty($data_array[0]))
							$condition[] = "`$name` >= '$data_array[0]'";
						if(!empty($data_array[1]))
							$condition[] = "`$name` <= '$data_array[1]'";
						if( !empty($data_array[0]) || !empty($data_array[1]) ){
							$sql[] = "(" . implode(" and ", $condition) . ")";
						}
					}else if(str_replace($keys,"",$condition) != $condition){
						$sql[] = "`$name` $condition";
					}else {
						$sql[] = "`$name` = $condition";
					}
				}
				$sql = implode(" $andOr ", $sql);
				break;
		}
		return $sql;
	}

}
